package jxcmanager.jxc.config.security;


import jxcmanager.jxc.config.ClassPathTldsLoader;
import jxcmanager.jxc.filter.CaptchaCodeFilter;
import jxcmanager.jxc.pojo.User;
import jxcmanager.jxc.service.IRbacservice;
import jxcmanager.jxc.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
import java.util.stream.Collectors;

@EnableGlobalMethodSecurity(prePostEnabled = true)  //开启使用security注解配置
@SpringBootConfiguration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JxcAuthenticationSuccessHandler jxcAuthenticationSuccessHandler;
    @Autowired
    private JxcAuthenticationFailedHandler jxcAuthenticationFailedHandler;
    @Resource
    private JxcLoginoutSuccessHandler jxcLoginoutSuccessHandler;
    @Resource
    private CaptchaCodeFilter captchaCodeFilter;
    @Resource
    private DataSource dataSource;
    @Resource
    private IUserService userService;
    @Resource
    private IRbacservice rbacservice;
    /**
     * 放行静态资源
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**",
                                                "/error/**",
                                                "/images/**",
                                                "/js/**",
                                                "/lib/**");
    }

    /**
     * 对登录拦截信息的配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用csrf
        http.csrf().disable()
                .addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class)  //用户认证前验证码
                //允许iframe页面嵌套
                .headers().frameOptions().disable()
                .and()
                    .formLogin()
                    .usernameParameter("userName")
                    .passwordParameter("password")
                    .loginPage("/index")
                    .loginProcessingUrl("/login")
                    .successHandler(jxcAuthenticationSuccessHandler)
                    .failureHandler(jxcAuthenticationFailedHandler)
                .and()
                    .logout()
                    .logoutUrl("/signout")
                    .deleteCookies("JSESSIONID")   //删除session对应的cookie
                    .logoutSuccessHandler(jxcLoginoutSuccessHandler)
                .and()
                    .rememberMe()
                //开启免登录
                    .rememberMeParameter("rememberMe")
                //勾选免登录按钮后，后端发送到前端的参数名称为rememberMe
                    .rememberMeCookieName("remember-me-cookie")
                    .tokenValiditySeconds(7 * 24 * 60 * 60 )
                    .tokenRepository(persistentTokenRepository())
                //写入用户登录的信息（token）到数据库的对应表中，此表是由spring security提供的
                .and()
                    .authorizeRequests().antMatchers("/index","/login","/image").permitAll()
                    .anyRequest().authenticated();   //对除了登录和首页外的资源拦截
    }

    /**
     * 从数据库中获取token
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }
    /**
     * 登录逻辑的实现
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
                User userDetails = userService.findUserByUserName(userName);
                /**
                 * 1.查询用户分配的角色
                 * 2.根据用户扮演的角色查询角色拥有的权限记录t
                 */
                List<String> roleNames = rbacservice.findRoleByUserName(userName);
                List<String> authorities = rbacservice.findAuthoritiesByRoleName(roleNames);
                //通过流对当前角色进行转换，转换为SpringSecurity的要求（commaSeparatedStringToAuthorityList放入角色时需要加前缀ROLE_）
                roleNames = roleNames.stream().map(role->
                    "ROLE_"+ role
                ).collect(Collectors.toList());
                authorities.addAll(roleNames);  //当前登录用户拥有的角色以及权限
                //String.join(参数1，参数2)  在参数2的字符串中以参数1为字符间隔，产生串联的单个字符串
                userDetails.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",",authorities)));
                return userDetails;
            }
        };
    }

    /**
     * 对密码进行加密的实现方法
     * （1）加密(encode)：注册用户时，使用SHA-256+随机盐+**把用户输入的密码进行hash处理，得到密码的hash值，然后将其存入数据库中。
     * （2）密码匹配(matches)：用户登录时，密码匹配阶段并没有进行密码解密（因为密码经过Hash处理，是不可逆的），而是使用相同的算法
     *      把用户输入的密码进行hash处理，得到密码的hash值，然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同，说明用户输入
     *      的密码正确。
     * @return
     */
    @Bean
    public PasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 框架使用userDetailsService
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(encoder());
    }

    /**
     * 加载 ClassPathTldsLoader
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(ClassPathTldsLoader.class)
    public ClassPathTldsLoader classPathTldsLoader(){
        return new ClassPathTldsLoader();
    }
}


